/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    LabelRanker.java
 *    Copyright (C) 2009-2012 Beijing Jiaotong University, China
 */

package fubin.classifiers;

import weka.classifiers.functions.SMO;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.TechnicalInformation;
import weka.core.Utils;
import mulan.classifier.InvalidDataException;
import mulan.classifier.MultiLabelLearner;
import mulan.classifier.MultiLabelLearnerBase;
import mulan.classifier.MultiLabelOutput;
import mulan.classifier.transformation.BinaryRelevance;
import mulan.data.MultiLabelInstances;

/**e
 * class that implements the label ranker approach for multi-label learning
 * 
 * @author fubin
 * 
 */
@SuppressWarnings("serial")
public class LabelRanker extends MultiLabelLearnerBase
{
	/** instances used to train classifiers */
	protected Instances m_instances;

	/**
	 * transition matrix, whose elements indicate the probability of transferring
	 * from one label to another
	 */
	protected double[][] m_transitionMatrix;

	/** the starting probability */
	protected double[] m_restartProbs;

	/***/
	protected double[] m_previousProbs;

	/** the final probabilities of every label */
	protected double[] m_currentProbs;

	/** the parameter controls the weight of propagation */
	protected double m_damping;

	/** the classifier used to learn the starting probabilities of every label */
	MultiLabelLearner m_baseLearner;


	/**
	 * initial the variables and allocate memories for them
	 * 
	 * @param trainingSet
	 */
	protected void init(MultiLabelInstances trainingSet)
	{
		m_instances = trainingSet.getDataSet();

		m_transitionMatrix = new double[numLabels][numLabels];
		for (int i = 0; i < numLabels; i++)
		{
			for (int j = 0; j < numLabels; j++)
				m_transitionMatrix[i][j] = 0;
		}
		m_restartProbs = new double[numLabels];
		m_previousProbs = new double[numLabels];
		m_currentProbs = new double[numLabels];

		m_baseLearner = new BinaryRelevance(new SMO());
	}


	/**
	 * set the damping factor
	 * 
	 * @param rating
	 */
	public void setDampingFactor(double dampingFactor)
	{
		m_damping = dampingFactor;
	}


	/**
	 * create the transition matrix of labels
	 */
	protected void createTransitionMatrix()
	{
		double[] columnSum = new double[numLabels];
		for (int i = 0; i < numLabels; i++)
			columnSum[i] = 0;

		/* calculate the cooccurrences of pairwise labels */
		int numInstances = m_instances.numInstances();
		for (int instIndex = 0; instIndex < numInstances; instIndex++)
		{
			Instance currentInst = m_instances.instance(instIndex);

			for (int i = 0; i < numLabels; i++)
			{
				double labelvaue = Double.parseDouble(currentInst.attribute(
				    labelIndices[i]).value((int) currentInst.value(labelIndices[i])));
				if (Utils.eq(labelvaue, 1.0))
					columnSum[i]++;
			}

			/* for every pair of labels, count the times they appear together */
			for (int i = 0; i < numLabels - 1; i++)
			{
				double leftValue = Double.parseDouble(currentInst.attribute(
				    labelIndices[i]).value((int) currentInst.value(labelIndices[i])));

				if (Utils.eq(leftValue, 1.0))
				{
					for (int j = i + 1; j < numLabels; j++)
					{
						double rightValue = Double.parseDouble(currentInst.attribute(
						    labelIndices[j])
						    .value((int) currentInst.value(labelIndices[j])));

						if (Utils.eq(leftValue, rightValue))
						{
							m_transitionMatrix[i][j]++;
						}
					}
				}
			}
		}

		/* set the value of the left-down matrix */
		for (int i = 0; i < numLabels - 1; i++)
		{
			for (int j = i + 1; j < numLabels; j++)
				m_transitionMatrix[j][i] = m_transitionMatrix[i][j];
		}

		/* normalise the matrix */
		for (int i = 0; i < numLabels; i++)
		{
			for (int j = 0; j < numLabels; j++)
			{
				m_transitionMatrix[j][i] /= columnSum[i];
			}
		}
	}


	@Override
	protected void buildInternal(MultiLabelInstances trainingSet)
	    throws Exception
	{
		/* step 1: initial the variables */
		init(trainingSet);

		/* step 2:calculate the transition matrix */
		createTransitionMatrix();

		/* step 3 train the base classifier */
		m_baseLearner.build(trainingSet);
	}


	/* examine whether labels' probability are converged to a stationary value */
	protected boolean isCoveraged()
	{
		boolean result = true;
		for (int i = 0; i < numLabels; i++)
		{
			double diff = Math.abs(m_previousProbs[i] - m_currentProbs[i]);
			double rate = diff / m_previousProbs[i];
			if (rate >= 0.001)
			{
				result = false;
				break;
			}
		}

		return result;
	}


	@Override
	protected MultiLabelOutput makePredictionInternal(Instance instance)
	    throws Exception, InvalidDataException
	{
		/* get the preferences */
		double[] basepredictions = m_baseLearner.makePrediction(instance)
		    .getConfidences();

		/* set every label's restart probability to be its initial prediction */
		for (int i = 0; i < numLabels; i++)
		{
			m_restartProbs[i] = basepredictions[i];
		}

		/* for the first iteration, every label's probability is equal */
		for (int i = 0; i < numLabels; i++)
			m_previousProbs[i] = 1 / numLabels;

		/* compute the second iteration */
		for (int i = 0; i < numLabels; i++)
		{
			double temp = 0;
			for (int j = 0; j < numLabels; j++)
			{
				temp += m_transitionMatrix[i][j] * m_previousProbs[j];
			}
			m_currentProbs[i] = m_damping * temp / (numLabels - 1) + (1 - m_damping)
			    * m_restartProbs[i];
		}

		/* repeat the process until converges */
		while (!isCoveraged())
		{
			for (int i = 0; i < numLabels; i++)
				m_previousProbs[i] = m_currentProbs[i];

			for (int i = 0; i < numLabels; i++)
			{
				double temp = 0;
				for (int j = 0; j < numLabels; j++)
				{
					temp += m_transitionMatrix[i][j] * m_previousProbs[j];
				}
				m_currentProbs[i] = m_damping * temp / (numLabels - 1)
				    + (1 - m_damping) * m_restartProbs[i];
			}
		}

		/* return the results */
		MultiLabelOutput mlo = new MultiLabelOutput(m_currentProbs, 0.5);
		return mlo;
	}


	@Override
	public TechnicalInformation getTechnicalInformation()
	{
		return null;
	}


	@Override
	public String globalInfo()
	{
		return null;
	}


	public static void main(String[] args)
	{
		try
		{

			/* set the weight to control propagation flow */
			double damping = 0;
			String dampingstring = Utils.getOption("a", args);
			if (dampingstring.length() != 0)
				damping = Double.parseDouble(dampingstring);
			else
				damping = 5;

			LabelRanker learner = new LabelRanker();
			learner.setDampingFactor(1);

			System.out.println("the damping is 0.3");
			System.out.println("the classfier for evaluation is:"
			    + learner.getClass().toString());
			runMultiLabelClassifer(learner, args);

		}
		catch (Exception e)
		{
		}
	}
}
